{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "[this doc on github](https://github.com/dotnet/interactive/blob/main/samples/notebooks/polyglot/github%20repo%20milestone%20report.ipynb)\n",
    "\n",
    "# Github repository report\n",
    "\n",
    "This notebook produces a project report based on your GitHub repository and attempts to create a burndown and completion estimate for a specified milestone. "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "[Generate a user token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) to get rid of public [API](https://github.com/octokit/octokit.net/blob/master/docs/getting-started.md) throttling policies for anonymous users "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "# The goal is\n",
    " * display milestones\n",
    " * show milestone burndown\n",
    " * what are possible completion dates for milestone\n",
    " * milestone work broken down by area\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "dotnet_interactive": {
     "language": "mermaid"
    }
   },
   "outputs": [],
   "source": [
    "flowchart \n",
    "    parameters[Gather org and repo ids] --> creategithubclient[create github client] \n",
    "    creategithubclient[create github client]  --> collect[(collect milestones)]\n",
    "    collect[(collect milestones)] --> collectissues[(collect milestone issues)]\n",
    "    collectissues[(collect milestone issues)] --> process[process milestone issues]\n",
    "    process[process milestone issues] --> derive[calculate burndown]\n",
    "    derive[calculate burndown] --> output[display burndown and milestone work by tag]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "#!set --name organization --value @input:\"Enter the name for your GitHub organization (e.g. dotnet)\"\n",
    "#!set --name repositoryName --value @input:\"Enter the name of the repository (e.g. interactive)\"\n",
    "#!set --name token --value @password:{\"prompt\":\"Enter your GitHub API token\", \"saveAs\": \"GitHub API Token\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "## Setup\n",
    "Importing packages and setting up connection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "#r \"nuget: Octokit, 13.0.1\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "using Octokit;\n",
    "using Microsoft.DotNet.Interactive.Formatting;\n",
    "using Microsoft.DotNet.Interactive.Formatting.TabularData;\n",
    "using Microsoft.DotNet.Interactive;\n",
    "using Microsoft.DotNet.Interactive.Commands;\n",
    "using System.Collections.Generic;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "dotnet_interactive": {
     "language": "javascript"
    }
   },
   "outputs": [],
   "source": [
    "plotlyloader = (require.config({\n",
    "    paths: {\n",
    "        d3: 'https://cdn.jsdelivr.net/npm/d3@7.4.4/dist/d3.min',\n",
    "        jquery: 'https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min',\n",
    "        plotly: 'https://cdn.plot.ly/plotly-2.14.0.min'\n",
    "    },\n",
    "\n",
    "    shim: {\n",
    "        plotly: {\n",
    "            deps: ['d3', 'jquery'],\n",
    "            exports: 'plotly'\n",
    "        }\n",
    "    }\n",
    "}) || require);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "var options = new ApiOptions();\n",
    "var gitHubClient = new GitHubClient(new ProductHeaderValue(\"notebook\"));\n",
    "\n",
    "if (!string.IsNullOrEmpty(token.GetClearTextPassword())) {\n",
    "    Console.WriteLine(\"Using GitHub API token\");\n",
    "    var tokenAuth = new Credentials(token.GetClearTextPassword());\n",
    "    gitHubClient.Credentials = tokenAuth;\n",
    "} else {\n",
    "    Console.WriteLine(\"Using anonymous GitHub API\");\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "var milestones = (await gitHubClient.Issue.Milestone.GetAllForRepository(organization, repositoryName, options)).Select(m => new{\n",
    "    Milestone = m,\n",
    "    Issues = (gitHubClient.Issue.GetAllForRepository(organization, repositoryName, new RepositoryIssueRequest {\n",
    "        Milestone= m.Number.ToString(),\n",
    "        State = ItemStateFilter.All\n",
    "    }, options)).Result.ToArray()\n",
    "}).ToArray();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "var milestoneData = milestones.Select(m =>{\n",
    "    var lastCountOpen = -1;\n",
    "    var startDate = m.Milestone.CreatedAt.DateTime;\n",
    "    var endDate = DateTime.Now.Date;\n",
    "    var ClosedEveryDay = m.Issues.Where(i => i.ClosedAt.HasValue).GroupBy(i => i.ClosedAt.Value.Date).Select(g => new {Date = g.Key, Count = g.Count()}).OrderBy(e => e.Date).ToArray();\n",
    "    \n",
    "    //var OpenedEveryDay = m.Issues.GroupBy(i => i.CreatedAt.Date).Select(g => new {Date = g.Key, Count = g.Count()}).OrderBy(e => e.Date).ToArray();\n",
    "\n",
    "    //var dates = ClosedEveryDay.Select(e => e.Date).Union(OpenedEveryDay.Select(e => e.Date)).Distinct().OrderBy(d => d).ToArray();\n",
    "    \n",
    "    var RollingClosedIssues = ClosedEveryDay.Select(e => new {e.Date, Count = ClosedEveryDay.Where(d => d.Date <= e.Date).Select(d => d.Count).Sum()}).ToArray();\n",
    "    var RollingOpenIssues = Enumerable.Range(0, (int)(endDate - startDate).TotalDays).Select( i => {\n",
    "        var date = startDate.AddDays(i);\n",
    "        var openedCount =  m.Issues.Where(i => (i.ClosedAt.HasValue == false) || (i.ClosedAt > date)).Count();\n",
    "        return new {Date = date, Count = openedCount};\n",
    "    }).Where(e => {\n",
    "        if(e.Count == lastCountOpen){\n",
    "            return false;\n",
    "        }else{\n",
    "            lastCountOpen = e.Count;\n",
    "            return true;\n",
    "        }\n",
    "    }).ToArray();\n",
    "   \n",
    "    // OpenedEveryDay.Select(e => new {e.Date, Count = OpenedEveryDay.Where(d => d.Date <= e.Date).Select(d => d.Count).Sum() - ClosedEveryDay.Where(d => d.Date <= e.Date).Select(d => d.Count).Sum()}).ToArray();\n",
    "\n",
    "    var isClosed = m.Milestone.State.ToString().ToLowerInvariant() == \"closed\";\n",
    "    var extrapolations = new List<(DateTime Date, int Count)>();\n",
    "    var AtRisk = false;\n",
    "    if(!isClosed){\n",
    "        var closingIssueSpeed = 0.0;\n",
    "        var alpha = 0.40;\n",
    "        for(var i = 0; i < RollingClosedIssues.Length - 1; i++){\n",
    "            var current = RollingClosedIssues[i];\n",
    "            var next = RollingClosedIssues[i + 1];\n",
    "            var days = (next.Date - current.Date).TotalDays;\n",
    "            if(days > 0){\n",
    "                var currentSpeed = (double)(next.Count - current.Count) / days;\n",
    "                closingIssueSpeed = ((1.0-alpha)*currentSpeed) + (alpha*closingIssueSpeed) ;\n",
    "            }\n",
    "        }\n",
    "        closingIssueSpeed = Math.Round(closingIssueSpeed,4, MidpointRounding.AwayFromZero);\n",
    "        var lastSample = RollingOpenIssues.Last();\n",
    "        Console.WriteLine($\"Milestone {m.Milestone.Title} is {m.Milestone.State}. Closing speed is {closingIssueSpeed} issues per day at {lastSample.Date}.\");\n",
    "\n",
    "        extrapolations = new List<(DateTime Date, int Count)>{\n",
    "            (lastSample.Date, lastSample.Count)\n",
    "        };\n",
    "\n",
    "        // take into account any pause to today\n",
    "\n",
    "        for(var i = 0; i < (int)((endDate - lastSample.Date).TotalDays); i++){\n",
    "            var nextCount = lastSample.Count;\n",
    "            extrapolations.Add((lastSample.Date.AddDays(i), nextCount));\n",
    "        }\n",
    "\n",
    "        for(var i = 0; i < extrapolations.Count - 1; i++){\n",
    "            var current = extrapolations[i];\n",
    "            var next = extrapolations[i + 1];\n",
    "            var days = (next.Date - current.Date).TotalDays;\n",
    "            if(days > 0){\n",
    "                closingIssueSpeed = alpha*closingIssueSpeed ;\n",
    " \n",
    "            }\n",
    "        }\n",
    "\n",
    "        closingIssueSpeed = Math.Round(closingIssueSpeed,2, MidpointRounding.AwayFromZero);\n",
    "        Console.WriteLine($\"Milestone {m.Milestone.Title} is {m.Milestone.State} and has {extrapolations.Count} extrapolated points. Closing speed is {closingIssueSpeed} issues per day.\");\n",
    "         \n",
    "        var lastExtrapolatedSample = extrapolations.Last();\n",
    "        var nextSample = lastExtrapolatedSample.Date.AddDays(1);\n",
    "        var closeDate = lastExtrapolatedSample.Date.AddMonths(1);\n",
    "        \n",
    "        if(closingIssueSpeed > 0){\n",
    "            var daysToClose = lastExtrapolatedSample.Count / closingIssueSpeed;\n",
    "            closeDate = lastExtrapolatedSample.Date.AddDays(daysToClose);\n",
    "            Console.WriteLine($\"Milestone {m.Milestone.Title} is {m.Milestone.State} will be closed by {closeDate}.\");\n",
    "        }else{\n",
    "            AtRisk = true;\n",
    "            Console.WriteLine($\"Milestone {m.Milestone.Title} will not be closed anytime soon.\");\n",
    "        }\n",
    "\n",
    "        var lastCount = lastExtrapolatedSample.Count;\n",
    "        while(nextSample < closeDate){\n",
    "            lastCount -= (int)(closingIssueSpeed);\n",
    "            extrapolations.Add((nextSample, lastCount));\n",
    "            nextSample = nextSample.AddDays(1);            \n",
    "        }\n",
    "    }\n",
    "\n",
    "    return new {\n",
    "        m.Milestone,\n",
    "        m.Issues,\n",
    "        ClosedEveryDay,\n",
    "       // OpenedEveryDay,\n",
    "        RollingClosedIssues,\n",
    "        RollingOpenIssues,\n",
    "        AtRisk,\n",
    "        ToComplete = extrapolations.Select(e => new {e.Date, e.Count}).ToArray()\n",
    "    };\n",
    "    }).ToArray();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "milestoneData.Select(m => new {m.Milestone.Title, m.Milestone.Description, m.Milestone.DueOn, m.Milestone.ClosedAt, m.Milestone.State,m.Milestone.OpenIssues, m.Milestone.ClosedIssues, m.Milestone.CreatedAt, m.AtRisk}).ToTabularDataResource().Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "var milestoneBurndown =  milestoneData.Where(m => m.Milestone.State == \"Open\")\n",
    ".OrderByDescending(m => m.Milestone.CreatedAt)\n",
    ".Select(m => new { Title = m.Milestone.Title, OpenIssues = m.RollingOpenIssues.ToArray(), ToComplete = m.ToComplete.ToArray(), m.AtRisk}).ToArray();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "dotnet_interactive": {
     "language": "javascript"
    }
   },
   "outputs": [],
   "source": [
    "#!share --from csharp milestoneBurndown"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "dotnet_interactive": {
     "language": "html"
    }
   },
   "outputs": [],
   "source": [
    "<div id=\"target\"></div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "dotnet_interactive": {
     "language": "javascript"
    },
    "tags": [
     "parameters"
    ]
   },
   "outputs": [],
   "source": [
    "  const traces = [];\n",
    "\n",
    "  const layout = {\n",
    "      title: 'Milestone Burndown',\n",
    "      grid: { rows: milestoneBurndown.length, columns: 1, pattern: 'independent' },\n",
    "      annotations: []\n",
    "    };\n",
    "\n",
    "    for(let i = 0; i < milestoneBurndown.length; i++) {       \n",
    "        layout[`xaxis${i+1}}`] = {};\n",
    "        layout[`yaxis${i+1}`] = { title: \"Open items\" };\n",
    "        const milestone = milestoneBurndown[i];        \n",
    "        const done = {\n",
    "            y: Array.from(milestone.OpenIssues.map(x => x.Count)),\n",
    "            x: Array.from(milestone.OpenIssues.map(x => x.Date)),\n",
    "            mode: 'lines',\n",
    "            //name: `Done [${milestone.Title}]`,\n",
    "            line: {\n",
    "              dash: 'solid',\n",
    "              width: 4\n",
    "            },\n",
    "            xaxis: `x${i+1}`,\n",
    "            yaxis: `y${i+1}`,\n",
    "            //hovertemplate: `<b>${milestone.Title}</b><br><i>Issue count</i>: %{y}<br><b>Date</b>: %{x}<extra></extra>`,\n",
    "            type: 'scattergl'\n",
    "        };\n",
    "\n",
    "        const toDo = {\n",
    "            y: Array.from(milestone.ToComplete.map(x => x.Count)),\n",
    "            x: Array.from(milestone.ToComplete.map(x => x.Date)),\n",
    "            mode: 'lines',\n",
    "            //name: `To Do [${milestone.Title}]`,\n",
    "            line: {\n",
    "              dash: 'dashdot',\n",
    "              width: 4\n",
    "            },\n",
    "            xaxis: `x${i+1}`,\n",
    "            yaxis: `y${i+1}`,\n",
    "            //hovertemplate: `<b>${milestone.Title} Projection</b>}<br><i>Issue count</i>: %{y}<br><b>Date</b>: %{x}<extra></extra>`,\n",
    "            type: 'scattergl'\n",
    "        };\n",
    "\n",
    "        layout.annotations.push({\n",
    "          x: done.x[0],\n",
    "          y: done.y.reduce((max, value) => {return Math.max(max, value)}),\n",
    "          yshift: 10 + done.line.width,\n",
    "          xanchor: 'left',\n",
    "          xref: `x${i+1}`,\n",
    "          yref: `y${i+1}`,\n",
    "          text: milestone.Title,\n",
    "          showarrow: false\n",
    "        });\n",
    "        \n",
    "        if(milestone.AtRisk) {\n",
    "          layout.annotations.push({\n",
    "            x: done.x[done.x.length - 1],\n",
    "            y: done.y[done.y.length - 1],\n",
    "            xanchor: 'center',\n",
    "            yanchor: 'bottom',\n",
    "            align: 'center',\n",
    "            xref: `x${i+1}`,\n",
    "            yref: `y${i+1}`,\n",
    "            text: \"\\u26A0\",\n",
    "            showarrow: true,\n",
    "            ax: 0,\n",
    "            ay: -(20 + done.line.width),\n",
    "            font: {\n",
    "              color: \"red\",\n",
    "              size: 30\n",
    "            }\n",
    "          });\n",
    "        }\n",
    "\n",
    "        traces.push(done);\n",
    "        traces.push(toDo);\n",
    "    }\n",
    "    \n",
    "exportData = { traces, layout };\n",
    "\n",
    "plotlyloader(['d3', 'plotly'], function (d3, plotly) {\n",
    "  console.log(\"Plotly loaded\"); \n",
    "  plotly.newPlot('target', exportData.traces, exportData.layout, {responsive: true});\n",
    "});\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "using Microsoft.DotNet.Interactive;\n",
    "using Microsoft.DotNet.Interactive.Commands;\n",
    "\n",
    "async Task PieWithMermaid(IEnumerable<(string area,int doneCount)> data, string label){\n",
    "    double total = data.Select(d => d.doneCount).Sum();\n",
    "    var slices = data.Select(d => $\"    \\\"{d.area}\\\" : {Math.Round((d.doneCount/total)*100.0, 2)}\").ToArray();\n",
    "\n",
    "    var mermaidPieMarkdown = new StringBuilder();\n",
    "    mermaidPieMarkdown.AppendLine(\"pie\");\n",
    "    mermaidPieMarkdown.AppendLine($@\"    title {label}\");\n",
    "\n",
    "    foreach(var slice in slices){\n",
    "        mermaidPieMarkdown.AppendLine(slice);\n",
    "    }\n",
    "    await Kernel.Root\n",
    "                .FindKernelByName(\"mermaid\")\n",
    "                .SendAsync(new SubmitCode(mermaidPieMarkdown.ToString()));\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    }
   },
   "outputs": [],
   "source": [
    "\n",
    "foreach(var milestone in milestoneData.OrderBy(m => m.Milestone.CreatedAt)){\n",
    "    var doneIssues = milestone.Issues.Where(i => i.ClosedAt.HasValue).ToArray();\n",
    "    if(doneIssues.Length > 0) {\n",
    "        var doneData = doneIssues.SelectMany( i => i.Labels.Select(l => l.Name)).Where(l => l.StartsWith(\"Area-\")).GroupBy(l => l).Select(l => (l.Key,l.Count()));\n",
    "        await PieWithMermaid(doneData, $\"Milestone: {milestone.Milestone.Title} work done by tag ({doneIssues.Length} of { milestone.Issues.Count()} items)\");\n",
    "    }\n",
    "   \n",
    "    var toDoIssues = milestone.Issues.Where(i => i.ClosedAt.HasValue == false).ToArray();\n",
    "    if(toDoIssues.Length > 0) {\n",
    "        var toDoData = toDoIssues.SelectMany( i => i.Labels.Select(l => l.Name)).Where(l => l.StartsWith(\"Area-\")).GroupBy(l => l).Select(l => (l.Key,l.Count()));\n",
    "        await PieWithMermaid(toDoData, $\"Milestone: {milestone.Milestone.Title} work to do  by tag({toDoIssues.Length} of { milestone.Issues.Count()} items)\");\n",
    "    }\n",
    "}"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".NET (C#)",
   "language": "C#",
   "name": ".net-csharp"
  },
  "polyglot_notebook": {
   "kernelInfo": {
    "defaultKernelName": "csharp",
    "items": [
     {
      "aliases": [],
      "name": ".NET"
     },
     {
      "aliases": [
       "C#",
       "c#"
      ],
      "languageName": "C#",
      "name": "csharp"
     },
     {
      "aliases": [
       "F#",
       "f#"
      ],
      "languageName": "F#",
      "name": "fsharp"
     },
     {
      "aliases": [],
      "languageName": "HTML",
      "name": "html"
     },
     {
      "aliases": [
       "js"
      ],
      "languageName": "JavaScript",
      "name": "javascript"
     },
     {
      "aliases": [],
      "languageName": "KQL",
      "name": "kql"
     },
     {
      "aliases": [],
      "languageName": "Mermaid",
      "name": "mermaid"
     },
     {
      "aliases": [
       "powershell"
      ],
      "languageName": "PowerShell",
      "name": "pwsh"
     },
     {
      "aliases": [],
      "languageName": "SQL",
      "name": "sql"
     },
     {
      "aliases": [],
      "name": "value"
     }
    ]
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
